﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Infrastructure;
using MugenMvvmToolkit.Interfaces.ViewModels;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.Infrastructure
{
    /// <summary>
    /// Represents base class for validate objects use data anotation attributes.
    /// </summary>
    public class DataAnnotationValidationProvider : IValidationProvider
    {
        #region Nested type: PropertyCache

        /// <summary>
        /// Represents the property cache.
        /// </summary>
        protected sealed class PropertyCache
        {
            #region Fields

            /// <summary>
            /// Gets the property name.
            /// </summary>
            public readonly string Name;

            /// <summary>
            /// Gets the original-property.
            /// </summary>
            public readonly PropertyInfo OriginalProperty;

            /// <summary>
            /// Gets or sets the attributes.
            /// </summary>
            public readonly List<ValidationAttribute> Attributes;

            /// <summary>
            /// Gets the display name.
            /// </summary>
            public string DisplayName { get; set; }

            #endregion

            #region Constructors

            /// <summary>
            /// Initializes a new instance of the <see cref="PropertyCache"/> class.
            /// </summary>
            public PropertyCache(PropertyInfo originalProperty, string displayName, List<ValidationAttribute> attributes)
            {
                OriginalProperty = originalProperty;
                Name = originalProperty.Name;
                DisplayName = displayName;
                Attributes = attributes;
            }

            #endregion
        }

        #endregion

        #region Fields

#if NETFX_CORE
        /// <summary>
        /// Represents the key to access to <see cref="IServiceProvider"/> from the validation metadata.
        /// </summary>
        public const string ServiceProviderKey = "_ServiceProviderKey_";
#endif

        [ThreadStatic]
        private static Dictionary<object, object> _contextDictionary;

        private static readonly ICollection<string> IgnoreProperties = new string[0];
        private static readonly Dictionary<Type, Dictionary<string, IList<PropertyCache>>> PropertiesCache = new Dictionary<Type, Dictionary<string, IList<PropertyCache>>>();

        private readonly IDisplayNameProvider _displayNameProvider;
        private readonly IServiceProvider _serviceProvider;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DataAnnotationValidationProvider"/> class.
        /// </summary>
        public DataAnnotationValidationProvider([NotNull]IDisplayNameProvider displayNameProvider, [CanBeNull] IServiceProvider serviceProvider)
        {
            Should.NotBeNull(displayNameProvider, "displayNameProvider");
            _displayNameProvider = displayNameProvider;
            _serviceProvider = serviceProvider;
        }

        #endregion

        #region Implementation of IValidationProvider

        /// <summary>
        ///     Determines whether the specified object is valid using the validation context.
        /// </summary>
        /// <param name="instance">The object to validate.</param>
        /// <param name="breakOnFirstError">
        ///     If <c>true</c> break check on first error, otherwise <c>false</c>.
        /// </param>
        /// <param name="validationResults">The collection to write validation results.</param>
        /// <param name="validationMetadata">The specified validation metadata. It's optional parameter.</param>
        public virtual void ValidateObject(object instance, bool breakOnFirstError, IList<IValidationResult> validationResults, IDictionary<object, object> validationMetadata)
        {
            Should.NotBeNull(instance, "instance");
            Should.NotBeNull(validationResults, "validationResults");
            var contextDictionary = GetContextDictionary(validationMetadata);
            try
            {
#if !NETFX_CORE
                var context = new ValidationContext(instance, _serviceProvider, contextDictionary);
#else
                contextDictionary[ServiceProviderKey] = _serviceProvider;
                var context = new ValidationContext(instance, contextDictionary);
                foreach (var o in contextDictionary)
                    context.Items[o.Key] = o.Value;
#endif
                var properties = GetOrAddProperties(instance);
                foreach (var propertyCache in properties.Values.SelectMany(list => list))
                {
                    object value = propertyCache.OriginalProperty.GetValue(instance, null);
                    if (!ValidatePropertyInternal(value, context, propertyCache, validationResults) && breakOnFirstError)
                        return;
                }
                context.MemberName = null;
                var validatableObject = instance as IValidatableObject;
                if (validatableObject != null)
                {
                    foreach (var validationResult in validatableObject
                        .Validate(context)
                        .Where(result => result != ValidationResult.Success)
                        .Select(result => new ValidationResultWrapper(result)))
                        validationResults.Add(validationResult);
                }
            }
            finally
            {
                if (validationMetadata == null)
                    contextDictionary.Clear();
            }
        }

        /// <summary>
        ///     Determines whether the specified object property is valid using the validation context.
        /// </summary>
        /// <param name="instance">The object to validate.</param>
        /// <param name="propertyName">The specified property name to validate.</param>
        /// <param name="validationResults">The collection to write validation results.</param>
        /// <param name="validationMetadata">The specified validation metadata. It's optional parameter.</param>
        public virtual void ValidateProperty(object instance, string propertyName, IList<IValidationResult> validationResults, IDictionary<object, object> validationMetadata)
        {
            Should.NotBeNull(instance, "instance");
            Should.NotBeNull(validationResults, "validationResults");
            if (string.IsNullOrEmpty(propertyName)) return;
            var properties = GetOrAddProperties(instance);
            IList<PropertyCache> propertyCaches;
            if (!properties.TryGetValue(propertyName, out propertyCaches))
                return;
            var contextDictionary = GetContextDictionary(validationMetadata);
            try
            {
#if !NETFX_CORE
                var context = new ValidationContext(instance, _serviceProvider, contextDictionary);
#else
                contextDictionary[ServiceProviderKey] = _serviceProvider;
                var context = new ValidationContext(instance, contextDictionary);
                foreach (var o in contextDictionary)
                    context.Items[o.Key] = o.Value;
#endif
                foreach (var propertyCache in propertyCaches)
                {
                    ValidatePropertyInternal(propertyCache.OriginalProperty.GetValue(instance, null), context, propertyCache, validationResults);
                }

            }
            finally
            {
                if (validationMetadata == null)
                    contextDictionary.Clear();
            }
        }

        /// <summary>
        ///     Fills errors from validation result to dictionary.
        /// </summary>
        /// <param name="validationResults">The collection with validation results.</param>
        /// <param name="dictionary">
        ///     An instance of <see cref="IDictionary{TKey,TValue}" />
        /// </param>
        public virtual void FillErrors(IList<IValidationResult> validationResults, IDictionary<string, IList<IValidationResult>> dictionary)
        {
            Should.NotBeNull(validationResults, "validationResults");
            Should.NotBeNull(dictionary, "dictionary");
            for (int index = 0; index < validationResults.Count; index++)
            {
                var validationResult = validationResults[index];
                if (validationResult.IsValid) continue;
                foreach (var result in validationResult.MemberNames.Where(s => s != null))
                {
                    IList<IValidationResult> values;
                    if (!dictionary.TryGetValue(result, out values))
                    {
                        values = new List<IValidationResult>();
                        dictionary[result] = values;
                    }
                    values.Add(validationResult);
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets or adds properties for the specified type.
        /// </summary>
        /// <param name="instance">The object to validate.</param>
        /// <returns>A series of instances of <see cref="PropertiesCache"/>.</returns>
        protected virtual Dictionary<string, IList<PropertyCache>> GetOrAddProperties(object instance)
        {
            var viewModel = instance as IValidatableViewModel;
            ICollection<string> ignoredProperties = viewModel != null ? viewModel.IgnoreProperties : IgnoreProperties;
            var type = instance.GetType();
            Dictionary<string, IList<PropertyCache>> dictionaryProperties;
            using (DeadLockDetector.Lock(PropertiesCache))
            {
                if (!PropertiesCache.TryGetValue(type, out dictionaryProperties))
                {
                    var propertyInfos = type.GetProperties().Where(info => !ignoredProperties.Contains(info.Name));
#if !NETFX_CORE
                    var properties = propertyInfos.Where(info => info.CanRead && info.GetGetMethod(true).GetParameters().Length == 0)
                            .Select(property => new PropertyCache(property, GetDisplayName(property), property.GetAttributes().OfType<ValidationAttribute>().ToList()))
                            .ToList();
                    var metadataTypeAttribute = type.GetAttributes()
                                                    .OfType<MugenMvvmToolkit.Attributes.MetadataTypeAttribute>()
#if WPF
.Concat(type.GetAttributes()
                                                        .OfType<System.ComponentModel.DataAnnotations.MetadataTypeAttribute>()
                                                        .Select(attribute => new Attributes.MetadataTypeAttribute(attribute.MetadataClassType)))
#endif
.ToArray();
#else
                    var properties = propertyInfos.Where(info => info.CanRead && info.GetMethod.GetParameters().Length == 0)
                            .Select(property => new PropertyCache(property, GetDisplayName(property), property.GetAttributes().OfType<ValidationAttribute>().ToList()))
                            .ToList();
                    var metadataTypeAttribute = type.GetTypeInfo().GetAttributes()
                                                    .OfType<MugenMvvmToolkit.Attributes.MetadataTypeAttribute>()
                                                    .ToArray();
#endif
                    foreach (var attribute in metadataTypeAttribute)
                    {
                        foreach (var metadataProperty in attribute.MetadataClassType.GetProperties())
                        {
                            bool isSet = false;
                            foreach (var value in properties.Where(cache => cache.Name == metadataProperty.Name))
                            {
                                isSet = true;
                                value.Attributes.AddRange(metadataProperty.GetAttributes().OfType<ValidationAttribute>());
                                value.DisplayName = GetDisplayName(metadataProperty, value.DisplayName);
                            }

                            if (!isSet)
                                Thrower.MissingMetadataProperty(type, metadataProperty.Name,
                                    attribute.MetadataClassType);
                        }
                    }
                    dictionaryProperties = new Dictionary<string, IList<PropertyCache>>();
                    foreach (var propertyCache in properties.Where(cache => cache.Attributes.Count != 0))
                    {
                        IList<PropertyCache> list;
                        if (!dictionaryProperties.TryGetValue(propertyCache.Name, out list))
                        {
                            list = new List<PropertyCache>();
                            dictionaryProperties[propertyCache.Name] = list;
                        }
                        list.Add(propertyCache);
                    }
                    PropertiesCache[type] = dictionaryProperties;
                }
            }
            return dictionaryProperties;
        }

        /// <summary>
        /// Gets a display name of the specified object using the specified property name.
        /// </summary>
        /// <param name="propertyInfo">The specified property.</param>
        /// <param name="oldName">The default name of property.</param>
        /// <returns>The display name of object.</returns>
        protected virtual string GetDisplayName(PropertyInfo propertyInfo, string oldName = null)
        {
            string displayName = _displayNameProvider.GetDisplayName(propertyInfo);
            if (displayName == propertyInfo.Name && oldName != null)
                return oldName;
            return displayName;
        }

        /// <summary>
        ///     Determines whether the specified object property is valid using the validation context.
        /// </summary>
        /// <param name="instance">The object to validate.</param>
        /// <param name="context">The specified validation context.</param>
        /// <param name="propertyCache">The specified <see cref="PropertiesCache"/>.</param>
        /// <param name="validationResults">The collection to write validation results.</param>
        protected virtual bool ValidatePropertyInternal(object instance, ValidationContext context, PropertyCache propertyCache, ICollection<IValidationResult> validationResults)
        {
            bool isValid = true;
            foreach (ValidationAttribute validationAttribute in propertyCache.Attributes)
            {
                context.MemberName = propertyCache.Name;
                context.DisplayName = propertyCache.DisplayName;
                ValidationResult validationResult = validationAttribute.GetValidationResult(instance, context);
                if (validationResult == ValidationResult.Success) continue;
                isValid = false;
                validationResults.Add(new ValidationResultWrapper(validationResult));
            }
            return isValid;
        }

        private static IDictionary<object, object> GetContextDictionary(IDictionary<object, object> objects)
        {
            if (objects != null)
                return objects;
            if (_contextDictionary == null)
                _contextDictionary = new Dictionary<object, object>();
            return _contextDictionary;
        }

        #endregion
    }
}